/*
一键Dump iOS加密MachO至A64Dbg与之对应的缓存目录。
*/

// 参见 def adcpp_ios_dump(data) 函数
#define dump_recv_name "adcpp_ios_dump"

#define logprefix "adcpp_ios_dump : "

// adcpp的api接口定义在：https://gitee.com/geekneo/A64Dbg/blob/master/adcpp/adcpp.hpp
#define dump2py(...) buf2py(dump_recv_name, __VA_ARGS__)

// adcpp.hpp已经自动包含了常用的C/C++/ObjC头文件，此处我们只需要额外包含一些不常用的头文件即可
#include <mach-o/dyld.h>
#include <mach/mach.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/task.h>
#include <mach/thread_act.h>
#include <mach/thread_status.h>

#define max_file_name 64

// 文件传输协议字段
struct dump_info {
  // 为了简化结构，直接固定名字长度
  char filename[max_file_name];

  // version_min_command
  // uint32_t  version;  /* X.Y.Z is encoded in nibbles xxxx.yy.zz */
  // 用于确定应该存放在decache的哪一个版本目录
  char min_version[12];

  // 文件大小和数据
  int filesize;
  char filebuff[0];
};

// 对当前指定的MachO进行Dump操作
static void do_dump(const char *path, const mach_header_64 *macho) {
  const char *name = strrchr(path, '/') + 1;
  if (strlen(name) >= max_file_name) {
    printf(logprefix "%s is too long, please reset the size of dump_info.name.\n", name);
    return;
  }
  printf(logprefix "Dumping %s.\n", path);

  FILE *file = fopen(path, "rb");
  if (!file) {
    printf(logprefix "Failed to read %s.\n", path);
    return;
  }
  uint32_t magic;
  fread(&magic, 1, sizeof(magic), file);
  if (magic != MH_MAGIC_64) {
    fclose(file);
    printf(logprefix "%s is not MH_MAGIC_64 magic, ignored dumping it.\n", name);
    return;
  }
  fseek(file, 0, SEEK_END);
  size_t filelength = ftell(file);
  fseek(file, 0, SEEK_SET);
  char *filebuff = new char[filelength];
  fread(filebuff, 1, filelength, file);
  fclose(file);

  printf(logprefix "Readed file %s, size %ld.\n", name, filelength);

  mach_header_64 *filehdr = (mach_header_64 *)filebuff;
  load_command *filelc = (load_command *)&filehdr[1];
  version_min_command *fileverlc = NULL;
  encryption_info_command_64 *fileenclc = NULL;
  for (auto i = 0; i < filehdr->ncmds; i++) {
    switch (filelc->cmd) {
      case LC_VERSION_MIN_IPHONEOS:
        fileverlc = (version_min_command *)filelc;
        break;
      case LC_ENCRYPTION_INFO_64:
        fileenclc = (encryption_info_command_64 *)filelc;
        break;
      default:
        break;
    }
    filelc = (load_command *)((char *)filelc + filelc->cmdsize);
  }
  if (!fileverlc) {
    static version_min_command verlc0 = { 0 };
    printf(logprefix
           "There's no LC_VERSION_MIN_IPHONEOS, reset to 0.\n");
    fileverlc = &verlc0;
  }
  if (!fileenclc) {
    printf(logprefix
           "Bad macho file, there's no LC_ENCRYPTION_INFO_64.\n");
    return;
  }
  printf(logprefix "Min version %x, encrypt info 0x%x,%d.\n", fileverlc->version,
         fileenclc->cryptoff, fileenclc->cryptsize);
  // 拷贝内存中已经解密的MachO数据
  memcpy(filebuff + fileenclc->cryptoff, (char *)macho + fileenclc->cryptoff, fileenclc->cryptsize);
  // 设置为非加密状态
  fileenclc->cryptid = 0;
  // 传回至adcpp_ios_dump函数进一步处理
  size_t dumpsize = sizeof(dump_info) + filelength;
  dump_info *dumpbuff = (dump_info *)malloc(dumpsize);
  dumpbuff->filesize = (int)filelength;
  sprintf(dumpbuff->min_version, "%d.%d.0", fileverlc->version >> 16,
          (uint8_t)(fileverlc->version >> 8));
  memcpy(dumpbuff->filebuff, filebuff, filelength);
  strcpy(dumpbuff->filename, name);
  printf(logprefix "Sending %s, %s, %d.\n", dumpbuff->filename, dumpbuff->min_version,
         dumpbuff->filesize);
  dump2py(dumpbuff, dumpsize);
  free(dumpbuff);
  delete[] filebuff;
}

// 暂停/恢复其他线程
static void thread_control(bool suspend) {
  mach_port_t threadcur = mach_thread_self();
  mach_port_t task = mach_task_self();
  thread_act_array_t threads;
  mach_msg_type_number_t count;
  kern_return_t kr = task_threads(task, &threads, &count);
  if (kr != KERN_SUCCESS) {
    printf(logprefix "Failed to get task threads with error %d, ignored thread control.\n", kr);
    return;
  }
  for (mach_msg_type_number_t i = 0; i != count; i++) {
    thread_t thread = threads[i];

    if (thread != threadcur) {
      if (suspend) {
        kr = thread_suspend(thread);
      } else {
        kr = thread_resume(thread);
      }
      printf(logprefix "%s task thread %08x with kernel result %d.\n",
             suspend ? "Suspending" : "Resuming", thread, kr);
    } else {
      printf(logprefix "Ignored adcpp thread %08x.\n", thread);
    }
  }

  for (mach_msg_type_number_t i = 0; i != count; i++) {
    mach_port_deallocate(task, threads[i]);
  }
  vm_deallocate(task, (vm_address_t)threads, count * sizeof(thread_t));
}

// 由于dump操作是一次性代码，所以不需要使用常驻内存的接口adc_main_thread
void adc_main() {
  printf(logprefix "Start dumping process %d (Build %s %s).\n", getpid(), __DATE__, __TIME__);
  thread_control(true);

  // 遍历所有App的MachO并解密Dump传回至adcpp_ios_dump接口
  // 然后由adcpp_ios_dump负责保存至A64Dbg对应的缓存目录
  for (auto i = 0; i < _dyld_image_count(); i++) {
    const char *path = _dyld_get_image_name(i);
    if (strstr(path, ".app/")) {
      do_dump(path, (mach_header_64 *)_dyld_get_image_header(i));
    }
  }

  thread_control(false);
  printf(logprefix "Finished dumping.\n");
}
